<?php
/**
 * Typical usage:
 * Route::bind('/files/$section/[$filename.$extension]')->to('Files')->with('filename', '[a-zA-Z]+')
 */
class Route {
	const VARIABLE_PATTERN = "/\\$([a-zA-Z]+)/";
	const DEFAULT_PATTERN = "([^\/]+)";

	/**
	 * Creates a new Route for a gives url.
	 */
	public static function bind($path) {
		return new Route($path);
	}
	
	private $module = '';
	private $limit = array();
	
	private $path;
	private $segments = array();
	private $patterns = array();
	private $order = array();
	private $regexp;
	private $multi = false; // Indicates if the route is using unbound segments
	
	private $match; // indicates if the route was matched successful
	private $result; // the result of the match
	
	/**
	 * Should only be instantiated through the static function 'bind'.
	 */
	private function __construct($path) {
		$this->path = trim($path, '/ ');
		if (empty($this->path)) {	
			throw new Exception("Can't bind a Route to an empty URL.");
		}
	}
	
	/**
	 * Adds a constrain to a variable.
	 */
	public function with($var, $pattern) {
		// detect if groupings are used in the regexp
		if (!(strpos($pattern, '(') === false && strpos($pattern, ')') === false)) {
			throw new Exception("Variable limitations can't contain groupings. ".$this->path." [$var/$pattern]");
		}
		
		$this->limit[$var] = '('.trim($pattern).')';
		return $this;
	}
	
	/**
	 * Which module to bind the Route to.
	 */
	public function to($module) {
		$this->module = trim($module);
		return $this;
	}
	
	/**
	 * Returns the associated module.
	 */
	public function module() {
		return $this->module;
	}
	
	public function matches($url) {
		if ($this->regexp === null) {
			$this->precalculate();
		}
		$this->result = array();
		$this->match = preg_match_all($this->regexp, $url, $this->result, PREG_PATTERN_ORDER);
		return $this->match;
	}
	
	public function variables() {
		if (!$this->match) {
			return array();
		}
		$index = 1;
		$variables = array();
		$result = $this->result; // make the expressions nicer
		
		if ($this->multi) {
			$norder = count($this->order);
			for ($i=0;$i<$norder-1;$i++) {
				$segment = $this->order[$i];
				foreach($segment as $variable) {
					$variables[$variable] = $result[$index++];
				}
			}
			// take care of the unbound segment
			$unbound = $this->result[$norder-1][0];
			$order = $this->order[$norder-1];
			$regexp = $this->patterns[$norder-1];
			$result = array();
			if (preg_match_all('/'.$regexp.'/', $unbound, $result, PREG_PATTERN_ORDER)) {
				$index = 1;
				foreach ($order as $variable) {
					$variables[$variable] = $result[$index++];
				}
			}
		} else {
			// no unbound variables
			foreach($this->order as $segment) {
				foreach($segment as $variable) {
					$variables[$variable]=$result[$index++];
				}
			}
		}
		return $variables;
	}
	
	public function url($variables) {
		if ($this->regexp === null) {
			$this->precalculate();
		}
		$url = '';
		$norder = count($this->order);
		
		$special = false;
		$unbounds = $this->order[$norder-1];
		if ($this->multi) {
			// check if we need special handling for unbound segments
			foreach($unbounds as $unbound) {
				if (!isset($variables[$unbound])) {
					// variable is missing
					return false;
				}
				if (is_array($variables[$unbound])) {
					$special = true;
					break;
				}				
			}
		}
		
		if ($special) {
			// unbound segments handle ordinary path first
			for ($i=0;$i<$norder-1;$i++) {
				$segment = $this->segments[$i];
				foreach($this->order[$i] as $variable) {
					if (!isset($variables[$variable])) {
						return false;
					}
					$segment = str_replace('$'.$variable, $variables[$variable], $segment);
				}
				$url .= '/'.$segment;
			}
			
			// count how many segments the unbound consists of
			$count = -1;
			foreach($unbounds as $unbound) {
				$c = count($variables[$unbound]);
				if ($count >= 0 && $c != $count) {
					throw new Exception("Variable arrays for unbound route segments must contain equals number of elements: (".$this->path.")");
				}
				$count = $c;
			}
			
			// append segments
			$template = $this->segments[$norder-1];
			for ($i=0;$i<$count;$i++) {
				$segment = $template;
				foreach($unbounds as $unbound) {
					$segment = str_replace('$'.$unbound, $variables[$unbound][$i], $segment);
				}
				$url .= '/'.$segment;
			}
		} else {
			// routes without unbound segments
			for ($i=0;$i<$norder;$i++) {
				$segment = $this->segments[$i];
				foreach($this->order[$i] as $variable) {
					if (!isset($variables[$variable])) {
						return false;
					}
					$segment = str_replace('$'.$variable, $variables[$variable], $segment);
				}
				$url .= '/'.$segment;
			}
		}
		
		return $url;
	}
	
	private function precalculate() {
		$this->regexp = '/';
		$this->multi = false;
		foreach(split('/', $this->path) as $segment) {
			if ($this->multi) {
				throw new Exception("An unbound segment must be the last in a route.");
			}
			if (empty($segment)) {
				throw new Exception("Route contains empty segment: ".$this->path);
			}
			
			// detect if the segment in unbound
			if ($segment[0] == '[' && substr($segment, -1, 1) == ']') {
				$segment = substr($segment, 1, count($segment)-2);
				$this->multi = true;
				$this->regexp .= '((\/';
			} else {
				$this->regexp .= '\/';
			}
			
			array_push($this->segments, $segment);

			// find all variable definitions that is used in the segment
			$variables = array();
			if (preg_match_all(self::VARIABLE_PATTERN, $segment, $variables, PREG_PATTERN_ORDER)) {
				$pattern = $this->buildVariablePattern($variables[1], $segment);
				array_push($this->patterns, $pattern);
				array_push($this->order, $variables[1]);					
				$this->regexp .= $pattern;
			} else {
				$quoted = preg_quote($segment);
				array_push($this->patterns, $quoted);
				array_push($this->order, array());
				$this->regexp .= $quoted;
			}
			
			if ($this->multi) {
				$this->regexp .= ')+)';
			}
		}
		$this->regexp .= '/';
	}
	
	private function buildVariablePattern($order, $segment) {
		$parts = preg_split(self::VARIABLE_PATTERN, $segment);
		$npart = count($parts);
		$norder = count($order);		
		$limit = $this->limit; // making the expression in the loop nicer
		$pattern = '';
		
		for($i=0;$i<$npart;$i++) {
			$pattern .= preg_quote($parts[$i]);
			if ($i < $norder) {
				$name = $order[$i];
				$pattern .= isset($limit[$name]) ? $limit[$name] : self::DEFAULT_PATTERN;
			}
		}	
		return $pattern;
	}
}
/*
$route = Route::bind('/files/$section/[$filename.$extension]')->to('Files')->with('filename', '[a-zA-Z]+');

$urls = array(
	'/files/a/myfile.txt',
	'/files/b/first.css/second.css'
);

foreach($urls as $tmp=>$url) {
	print ("Testing: $url\n");
	$ret = $route->matches($url)?"Yes":"No";
	print ("Matches: $ret\n");	
	print_r ($route->variables());
}


$var = array(
	'section'=> 'S',
	'filename' => 'file',
	'extension' => 'txt'
);
print($route->url($var)."\n");

$var2 = array(
	'section'=> 'S',
	'filename' => array('file1', 'file2'),
	'extension' => array('txt', 'exe')
);
print($route->url($var2)."\n");

*/

?>
